31. Project 2048#
In this project, you will develop the 2048 game in Python using a functional (without class) approach and two libraries for the graphical interface: rich to get a nice display in console and readchar for the keyboard input.
31.1. Objectives#
Understand functional programming in Python
Work with data structures
Use the Blessed library to create an interactive terminal interface
Decompose a problem into simple and testable functions
31.2. 2048 Game Rules Review#
On a 4x4 grid, a new tile (2 or 4) appears randomly each turn (2 appears with 90% of chance, 4 with 10%). The player can move all tiles in four directions (up, down, left, right). Two adjacent tiles of the same value merge into one (double their value). The game ends when no moves are possible. The goal is to reach tile 2048 (and continue beyond).
You can test the game at this address: 2048.
32. Instructions#
You must follow the tutorial for this project. It will provide you with a framework, particularly for the MVC pattern (Model, View, Controller). Follow this framework meticulously. The minimum objective of the project is to achieve a playable version with a text-based interface (see part 4). For more advanced students, it will be possible to add optional features (Tkinter interface, Pygame, AI, game saving, high scores, etc.) to improve your final grade.
33. Part 1 - Model#
The model will consist of pure functions that manipulate the game state, represented by a dictionary state.
33.1. Step 1.1: Data Structure and Initialization#
Goal: Create functions to initialize and manipulate the game state.
Game State Structure:
{
'grid': [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
'score': 0,
'game_over': False,
'size': 4
}
To Implement:
def create_game_state(size=4):
"""
Creates a new initial game state.
Args:
size (int): Grid size (default 4)
Returns:
dict: Initial game state
"""
pass
Tests to Perform:
Use the following code to test your code. Always remove the tests after.
state = create_game_state()
print(state["grid"]) # -> [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
print(state["score"]) # -> 0
print(state["game_over"]) # -> False
state = create_game_state(size=5)
print(state["grid"]) # -> [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
33.2. Step 1.2: Adding Random Tiles#
Goal: Implement functions to add a new tile. Remember to correctly organized your source code. Please ensure you adhere to the function parameters and expected return values. Everything is specified in the comment at the beginning of the function. Please keep it in your code; it is part of the documentation.
To Implement: Implement the following functions to add a tile.
Hint: Becarefull and keep the same order to identify a cell. I recommand first, the line, and then the column. So grid[0][2] means the tile in the first line and the last column.
import random
def get_empty_cells(grid):
"""
Returns the list of coordinates of empty cells.
Args:
grid (list): Game grid
Returns:
list: List of tuples (row, column)
"""
pass
def add_random_tile(state):
"""
Add a random tile (2 or 4) to an empty cell in the grid.
Args:
state (dict): Current game state
Returns:
bool: True if a tile was added, False otherwise
"""
pass
def update_score(state):
"""
Updates the score in the game state. The score is the sum of all tiles.
Args:
state (dict): Current game state
Returns:
None
"""
Tests to Perform:
state = create_game_state()
print(state["grid"]) # Should display a grid with all zeros
b = add_random_tile(state)
print(b) # Should display True
print(state["grid"]) # Should display a grid with one random tile (2 or 4)
print("---")
b = add_random_tile(state)
print(b) # Should display True
print(state["grid"]) # Should display two random tiles
print("---")
# Count non-zero tiles
count = sum(1 for row in state["grid"] for cell in row if cell != 0)
print(f"Number of tiles: {count}") # Should display 2
print("---")
state["grid"] = [[2, 4, 2, 4],
[4, 2, 4, 2],
[2, 4, 2, 4],
[4, 2, 4, 2]]
print(state["grid"]) # Filled random grid
b = add_random_tile(state)
print(state["grid"]) # Should display the same grid
print(b) # Should display False
print("---")
update_score(state)
print(state["score"]) # Should display 48
33.3. Step 1.3: Merging a Line to the Left#
Goal: Implement the logic for moving and merging a single line.
Suggested Algorithm:
Extract non-zero values
Merge adjacent identical values
Fill with zeros on the right
To Implement:
def merge_line_left(line):
"""
Moves and merges tiles in a line to the left.
Args:
line (list): A grid line
Returns:
tuple: new_line
Example:
[2, 0, 2, 4] -> [4, 4, 0, 0]
[2, 2, 4, 4] -> [4, 8, 0, 0]
"""
pass
Tests to Perform:
# Test 3: Line merging
result = merge_line_left([2, 0, 2, 4])
print(result) # [4, 4, 0, 0]
result = merge_line_left([2, 2, 4, 4])
print(result) # [4, 8, 0, 0]
result = merge_line_left([2, 4, 8, 16])
print(result) # [2, 4, 8, 16]
result = merge_line_left([0, 0, 0, 0])
print(result) # [0, 0, 0, 0]
33.4. Step 1.4: Grid Transformations#
Goal: Implement utility functions to manipulate the grid.
To Implement:
def transpose_grid(grid):
"""
Returns the transpose of the grid.
Args:
grid (list): Grid to transpose
Returns:
list: Transposed grid
"""
pass
def reverse_grid_rows(grid):
"""
Returns a grid with reversed rows.
Args:
grid (list): Original grid
Returns:
list: Grid with reversed rows
"""
pass
Tests to perform:
# Test 4: Transformations
grid = [[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]
transposed = transpose_grid(grid)
print(transposed) # [[1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15], [4, 8, 12, 16]]
reversed_grid = reverse_grid_rows(grid)
print(reversed_grid) # [[4, 3, 2, 1], [8, 7, 6, 5], [12, 11, 10, 9], [16, 15, 14, 13]]
33.5. Step 1.5: Movements in 4 Directions#
Goal: Implement movement functions for each direction.
Tips:
For move_right: reverse rows, apply move_left, reverse again
For move_up: transpose, apply move_left, transpose again
For move_down: transpose, apply move_right, transpose again
To Implement:
def move_left(state):
"""
Returns a new state after moving left.
Args:
state (dict): Current game state
Returns:
dict: New state after the move
"""
pass
def move_right(state):
"""Returns a new state after moving right."""
pass
def move_up(state):
"""Returns a new state after moving up."""
pass
def move_down(state):
"""Returns a new state after moving down."""
pass
Tests to perform:
# Test 5: Movements
state = create_game_state()
state['grid'] = [
[2, 0, 2, 0],
[4, 4, 0, 0],
[0, 0, 8, 8],
[2, 2, 2, 2]
]
print(f"Initial grid: {state['grid']}")
b = move_left(state)
print(f"Grid after left: {state['grid']}") # [[4, 0, 0, 0], [8, 0, 0, 0], [16, 0, 0, 0], [4, 4, 0, 0]]
print(f"Score: {state['score']}") # 36
print(f"Grid has changed: {b}") # True
print("---")
print(f"Initial grid: {state['grid']}")
b = move_right(state)
print(f"Grid after right: {state['grid']}") # [[0, 0, 0, 4], [0, 0, 0, 8], [0, 0, 0, 16], [0, 0, 0, 8]]
print(f"Score: {state['score']}") # 36
print(f"Grid has changed: {b}") # True
print("---")
state['grid'] = [
[0, 0, 2, 2],
[4, 4, 2, 4],
[0, 0, 8, 8],
[2, 4, 2, 2]
]
print(f"Initial grid: {state['grid']}")
b = move_up(state)
print(f"Grid after up: {state['grid']}") # [[4, 8, 4, 2], [2, 0, 8, 4], [0, 0, 2, 8], [0, 0, 0, 2]]
print(f"Score: {state['score']}") # 44
print(f"Grid has changed: {b}") # True
print("---")
print(f"Initial grid: {state['grid']}")
b = move_down(state)
print(f"Grid after down: {state['grid']}") # [[0, 0, 0, 2], [0, 0, 4, 4], [4, 0, 8, 8], [2, 8, 2, 2]]
print(f"Score: {state['score']}") # 44
print(f"Grid has changed: {b}") # True
print("---")
state["grid"] = [
[2, 4, 2, 4],
[4, 2, 4, 2],
[2, 4, 2, 4],
[4, 2, 4, 2]
]
print(f"Initial grid: {state['grid']}")
b = move_left(state)
print(f"Grid after left: {state['grid']}") # [[0, 0, 0, 2], [0, 0, 4, 4], [4, 0, 8, 8], [2, 8, 2, 2]]
print(f"Grid has changed: {b}") # False
print("---")
33.6. Step 1.6: End Game Detection#
Goal: Detect when no moves are possible.
To Implement:
def can_move(grid):
"""
Checks if at least one move is possible.
Args:
grid (list): Grid to check
Returns:
bool: True if a move is possible, False otherwise
"""
pass
def update_game_over(state):
"""
Returns a new state with game_over updated.
Args:
state (dict): Current state
Returns:
dict: New state with updated game_over
"""
pass
Tests to perform:
# Test 6: End detection
grid_with_moves = [
[2, 4, 2, 4],
[4, 2, 4, 2],
[2, 4, 2, 4],
[4, 2, 4, 0] # Empty cell
]
print(can_move(grid_with_moves)) # True
grid_no_moves = [
[2, 4, 2, 4],
[4, 2, 4, 2],
[2, 4, 2, 4],
[4, 2, 4, 2]
]
print(can_move(grid_no_moves)) # False
grid_with_vertical_moves = [
[4, 8, 2, 8],
[4, 2, 8, 2],
[2, 8, 2, 8],
[8, 2, 8, 2]
]
print(can_move(grid_with_vertical_moves)) # True
grid_with_horizontal_moves = [
[4, 4, 2, 8],
[8, 2, 8, 2],
[2, 8, 2, 8],
[8, 2, 8, 2]
]
print(can_move(grid_with_horizontal_moves)) # True
33.7. Step 1.7: Main Game Function#
Goal: Encapsulate all the logic for one move.
To Implement:
def play_move(state, direction):
"""
Performs a move in the given direction.
Args:
state (dict): Current game state
direction (str): 'up', 'down', 'left', 'right'
Returns:
dict: New state after the move
"""
# 1. Perform the move according to direction
# 2. If the grid has changed:
# - Add a new tile
# - Check game over
# 3. Return the new state
pass
Tests to perform:
# Test 7: Complete game
state = create_game_state()
state["grid"] = [
[4, 4, 2, 8],
[8, 2, 8, 2],
[2, 8, 2, 8],
[8, 2, 8, 2]
]
print("Initial state:")
print(state["grid"])
play_move(state, 'left')
print("\nAfter left move:")
print(state["grid"]) # Should be: [[8, 2, 8, X], [8, 2, 8, 2], [2, 8, 2, 8], [8, 2, 8, 2]] with x = 2 or 4
print(f"Game Over: {state['game_over']}") # Should be False
34. Part 2: The View with rich and readchar#
From this step, you will need to use a terminal to run the program. In other case, the command from rich and readchar libraries will not work.
34.1. Step 2.1: Install the libraries#
With Thonny, in the tools menu, you can open a configured terminal using open system shell. In the terminal, launch the two following commands to install the libraries.
pip3 install rich
pip3 install readchar
Create a view.py module containing the following import instructions.
import rich.box
from rich.console import Console
from rich.table import Table
from rich.text import Text
import readchar
Open a terminal and launch the command python3 view.py. If there is no error, then the module are correctly installed.
34.2. Step 2.2: Read a character from the keyboard#
In the module view.py add the following function and test it.
def get_player_input():
"""
Waits for and returns player input.
Returns:
str: Direction ('up', 'down', 'left', 'right') or 'quit'
"""
try:
key = readchar.readkey()
# Map keys to directions
key_mapping = {
readchar.key.UP: 'up',
readchar.key.DOWN: 'down',
readchar.key.LEFT: 'left',
readchar.key.RIGHT: 'right',
'q': 'quit',
'Q': 'quit'
}
return key_mapping.get(key, None)
except:
return None
def test_input():
"""
Waits for an arrow key until "q" or "Q" is input.
"""
k = get_player_input()
while k != "quit":
print(k)
k = get_player_input()
print(k)
34.3. Step 2.3: Grid Display#
We provide functions to help you to represent the grid using rich module. Copy/paste the following code into the view.py module.
# Color palette for tiles
TILE_STYLES = {
0: "dim white on grey23", # Empty
2: "black on grey78", # 2
4: "black on grey74", # 4
8: "white on dark_orange", # 8
16: "white on orange1", # 16
32: "white on red", # 32
64: "white on red3", # 64
128: "black on yellow", # 128
256: "black on gold1", # 256
512: "black on yellow1", # 512
1024: "black on gold3", # 1024
2048: "white on yellow1 bold", # 2048
}
def create_console():
"""
Creates a Rich console instance.
Returns:
Console: Console instance
"""
return Console()
def get_tile_style(value):
"""
Returns the style (color, background) for a tile value.
Args:
value (int): Tile value
Returns:
str: Rich style string
"""
if value not in TILE_STYLES:
# For values > 2048
return "white on magenta bold"
return TILE_STYLES[value]
def create_grid_table(state):
"""
Creates a Rich Table representing the game grid.
Args:
state (dict): Game state
Returns:
Table: Styled Rich table
"""
grid = state['grid']
# Create table without headers
table = Table(show_header=False, show_edge=True, show_lines=True, pad_edge=False,
box=rich.box.HEAVY_EDGE, padding=(0, 0))
# Add columns
for _ in range(len(grid)):
table.add_column(justify="center", width=6)
# Add rows with styled tiles
for row in grid:
styled_cells = []
for value in row:
if value == 0:
# Empty cell
cell = Text("·", style=get_tile_style(0))
else:
# Tile with value
cell = Text(str(value), style=get_tile_style(value))
styled_cells.append(cell)
table.add_row(*styled_cells)
return table
Tests to perform:
The following function allow you to create a test grid and display it on a terminal. Remember, you should use a terminal to execute your programm. Do not use the run current script buton (green arrow).
def test_grid_display():
"""
Test function to display a sample grid.
"""
state = {
'grid': [
[2, 0, 0, 2],
[4, 4, 0, 0],
[0, 0, 8, 8],
[16, 0, 0, 16]
],
'score': 0,
'game_over': False
}
console = create_console() # Create a console instance
table = create_grid_table(state) # Create the grid table
console.clear() # Clear the console
console.print(table) # Print the table to the console
You should see something like that.

35. Part 3: The Controller (Interaction Management)#
You have now all functionalities to implement the controller of the game.
35.1. Step 3.1: Keyboard Input Management with Blessed#
Goal: Create a controller.py module and use functions from model and view to implement the game.
To Implement:
import model
import view
def game_loop():
"""
Main game loop for 2048. Create a new game state, add two initial tiles,
and repeatedly get player input to play moves until the game is over. Then quits and display the player score.
:return:
"""
pass
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[8], line 1
----> 1 import model
2 import view
4 def game_loop():
ModuleNotFoundError: No module named 'model'
36. Part 4: Show your skills#
Your project is now complete in its minimal version.
You can now show your capabilities. You can implement any new features you choose. For example:
The ability to quit and resume a game later with a save file.
A high score table that is saved between launches.
A graphical interface using Pygame.
An AI that plays for you.
…
However, here are a few constraints:
The model/view/controller structure must be maintained.
The model must not change unless a specific reason is provided. It can, however, be expanded.
Documentation in the form of a README.txt file must document and explain the features you have chosen to implement.
At the end of your work, you will upload the directory containing all your source code and documentation in ZIP format (only zip, no 7zip or rar, or anything else). You will find on EDUNAO a link to upload your project. The deadline is the 4th january 2026 at 23h59.